β

Как стать автором
Погнали в космос?
Какой вы ML-специалист: составим портреты типичных ML’щиков

Моя лентаВсе потоки

59.73

Рейтинг

Nixys
DevOps, DevSecOps, MLOps — системный IT-интегратор

Подписаться

borisershov 29 июл 2019 в 13:53

Разбираемся с пакетом Context в Golang

11 мин

160K

Блог компании NixysGo*Программирование*

Перевод

Автор оригинала: Parikshit Agnihotry

Пакет context в Go полезен при взаимодействиях с API и медленными процессами, особенно в production-grade системах, которые занимаются веб-запросами. С его помощью можно уведомить горутины о необходимости завершить свою работу.

Ниже небольшое руководство, которое поможет использовать этот пакет в ваших проектах, а также расскажет о некоторых лучших практиках и подводных камнях.

(Прим. пер.: Контекст используется во многих пакетах, например, в работе с Docker).

Перед тем, как начать

Чтобы использовать контексты, вы должны понимать, что такое горутина и каналы. Я постараюсь кратко их рассмотреть. Если вы с ними уже знакомы, переходите непосредственно к разделу Контекст (Context).

Горутина

В официальной документации говорится, что «Горутина — это легковесный поток выполнения». Горутины легче, чем потоки, поэтому управление ими сравнительно менее ресурсозатратно.

→ Песочница

package main

import "fmt"

// Функция, которая выводит Hello
func printHello() {
fmt.Println("Hello from printHello")
}

func main() {
// Встроенная горутина
// Определяем функцию внутри и вызываем ее
go func(){fmt.Println("Hello inline")}()
// Вызываем функцию как горутину
go printHello()
fmt.Println("Hello from main")
}

Если вы запустите эту программу, то увидите, что распечатывается только Hello from main. На самом деле запускается обе горутины, но main завершает работу раньше. Значит, горутинам нужен способ сообщать main об окончании своего выполнения, и чтобы та ждала этого. Здесь нам на помощь приходят каналы (channels).

Каналы (channels)

Каналы — это способ коммуникации между горутинами. Они используются, когда вы хотите передать результаты, ошибки или другую информацию от одной горутины другой. Каналы бывают разных типов, например, канал типа int получает целые числа, а канал типа error — ошибки, и т.д.

Скажем, у нас есть канал ch типа int. Если вы хотите послать что-то в канал, синтаксис будет ch <- 1. Что-то получить из канала можно так: var := <- ch, т.е. забрать из канала значение и сохранить его в переменной var.

Следующий код иллюстрирует, как, используя каналы, получить подтверждение, что горутины завершили свою работу и вернули свои значения в main.

Прим.: Для синхронизации также могут использоваться группы ожидания, но в этой статье для примеров кода я выбрал каналы, поскольку дальше мы их используем в разделе о контекстах.

→ Песочница

package main

import "fmt"

// Печатает на стандартный вывод и отправляет int в канал
func printHello(ch chan int) {
fmt.Println("Hello from printHello")
// Посылает значение в канал
ch <- 2
}

func main() {
// Создаем канал. Для этого нам нужно использовать функцию make
// Каналы могут быть буферизированными с заданным размером:
// ch := make(chan int, 2), но это выходит за рамки данной статьи.
ch := make(chan int)

// Встроенная горутина. Определим функцию, а затем вызовем ее.
// Запишем в канал по её завершению
go func(){
fmt.Println("Hello inline")
// Отправляем значение в канал
ch <- 1
}()

// Вызываем функцию как горутину
go printHello(ch)
fmt.Println("Hello from main")

// Получаем первое значение из канала
// и сохраним его в переменной, чтобы позже распечатать
i := <- ch
fmt.Println("Received ",i)

// Получаем второе значение из канала
// и не сохраняем его, потому что не будем использовать
<- ch
}

Контекст (Context)

Пакет context в go позволяет вам передавать данные в вашу программу в каком-то «контексте». Контекст так же, как и таймаут, дедлайн или канал, сигнализирует прекращение работы и вызывает return.

Для примера, если вы делаете веб-запрос или выполняете системную команду, будет хорошей идеей использовать таймаут для production-grade систем. Потому что, если API, к которому вы обращаетесь, работает медленно, вы вряд ли захотите накапливать запросы у себя в системе, поскольку это может привести к увеличению нагрузки и снижению производительности при обработке собственных запросов. В результате возникает каскадный эффект.

И здесь как раз может пригодиться контекст тайм-аута или дедлайна.

Создание контекста

Пакет context позволяет создавать и наследовать контекст следующими способами:

context.Background() ctx Context

Эта функция возвращает пустой контекст. Она должна использоваться только на высоком уровне (в main или обработчике запросов высшего уровня). Он может быть использован для получения других контекстов, которые мы обсудим позже.

ctx, cancel := context.Background()

Прим. пер.: В оригинале статьи имеется неточность, правильный пример использования context.Background будет следующий:

ctx := context.Background()

context.TODO() ctx Context

Эта функция также создает пустой контекст. И она тоже должна использоваться только на высоком уровне или когда вы не уверены, какой контекст использовать, или если в функции еще нет получения нужного контекста. Это значит, что вы (или тот, кто поддерживает код) планируете позже добавить контекст в функцию.

ctx, cancel := context.TODO()

Прим. пер.: В оригинале статьи имеется неточность, правильный пример использования context.TODO будет следующий:

ctx := context.TODO()

Что интересно, взгляните на код, это абсолютно то же самое, что и background. Разница лишь в том, что в данном случае можно пользоваться инструментами статического анализа для проверки валидности передачи контекста, что является важной деталью, поскольку эти инструменты помогают выявлять потенциальные ошибки на ранней стадии и могут быть включены в CI/CD пайплайн.

Отсюда:

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

context.WithValue(parent Context, key, val interface{}) (ctx Context, cancel CancelFunc)

Прим. пер.: В оригинале статьи имеется неточность, правильная сигнатура для context.WithValue будет следующей:

context.WithValue(parent Context, key, val interface{}) Context

Эта функция принимает контекст и возвращает производный от него контекст, в котором значение val связано с key и проходит через всё контекстное дерево. То есть, как только вы создадите контекст WithValue, любой производный контекст получит это значение.

Не рекомендуется передавать критические параметры, используя значения контекста, вместо этого функции должны принимать их в сигнатуре явным образом.

ctx := context.WithValue(context.Background(), key, "test")

context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)

Здесь становится чуть интереснее. Эта функция создает новый контекст из переданного ей родительского. Родителем может быть контекст background или контекст, переданный в качестве аргумента функции.

Возвращается производный контекст и функция отмены. Вызывать функцию отмены контекста должна только та функция, которая его создает. Вы можете передавать функцию отмены другим функциям, если хотите, но это настоятельно не рекомендуется. Обычно это решение принимается от непонимания работы отмены контекста. Из-за этого порожденные от этого родителя контексты могут повлиять на программу, что приведет к неожиданному результату. Короче говоря, лучше НИКОГДА не передавайте функцию отмены.

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))

Прим. пер.: В оригинале статьи автор, видимо, ошибочно для context.WithCancel привёл пример с context.WithDeadline. Правильный пример для context.WithCancel будет следующим:

ctx, cancel := context.WithCancel(context.Background())

context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)

Эта функция возвращает производный контекст от своего родителя, который отменяется после дедлайна или вызова функции отмены. Например, вы можете создать контекст, который автоматически отменяется в определенное время и передает это дальше в дочерние функции. Когда этот контекст отменяется после дедлайна, все функции, у которых есть этот контекст, по уведомлению должны закончить работу.

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))

context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)

Эта функция похожа на context.WithDeadline. Разница в том, что в качестве входных данных используется длительность времени. Эта функция возвращает производный контекст, который отменяется при вызове функции отмены или по истечении времени.

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))

Прим. пер.: В оригинале статьи автор, видимо, ошибочно для context.WithTimeout привёл пример с context.WithDeadline. Правильный пример для context.WithTimeout будет следующим:

ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)

Приём и использование контекстов в ваших функциях

Теперь, когда мы знаем, как создавать контексты (Background и TODO) и как порождать контексты (WithValue, WithCancel, Deadline и Timeout), давайте обсудим, как их использовать.

В следующем примере вы можете видеть, что функция, принимающая контекст, запускает горутину и ожидает ее возврата или отмены контекста. Оператор select помогает нам определить, что случится первым, и завершить работу функции.

После закрытия канала Done <-ctx.Done() выбирается случай case <-ctx.Done():. Как только это происходит, функция должна прервать работу и подготовиться к возврату. Это означает, что вы должны закрыть любые открытые соединения, освободить ресурсы и вернуться из функции. Бывают случаи, когда освобождение ресурсов может задержать возврат, например, зависает очистка. Вы должны иметь это в виду.

Пример, который следует за этим разделом, это полностью готовая программа на go, которая иллюстрирует timeout’ы и функции отмены.

// Функция, выполняющая какую-то медленную работу с использованием контекста
// Заметьте, что контекст - это первый аргумент
func sleepRandomContext(ctx context.Context, ch chan bool) {

// Выполнение (прим. пер.: отложенное выполнение) действий по очистке
// Созданных контекстов больше нет
// Следовательно, отмена не требуется
defer func() {
fmt.Println("sleepRandomContext complete")
ch <- true
}()

// Создаем канал
sleeptimeChan := make(chan int)

// Запускаем выполнение медленной задачи в горутине
// Передаём канал для коммуникаций
go sleepRandom("sleepRandomContext", sleeptimeChan)

// Используем select для выхода по истечении времени жизни контекста
select {
case <-ctx.Done():
// Если контекст истекает, выбирается этот случай
// Высвобождаем ресурсы, которые больше не нужны из-за прерывания работы
// Посылаем сигнал всем горутинам, которые должны завершиться (используя каналы)
// Обычно вы посылаете что-нибудь в канал,
// ждете выхода из горутины, затем возвращаетесь
// Или используете группы ожидания вместо каналов для синхронизации
fmt.Println("Time to return")

case sleeptime := <-sleeptimeChan:
// Этот вариант выбирается, когда работа завершается до отмены контекста
fmt.Println("Slept for ", sleeptime, "ms")
}
}

Пример

Как мы увидели, с помощью контекстов можно работать с дедлайнами, таймаутами, а также вызвать функцию отмены, тем самым дав понять всем функциям, использующим производный контекст, что надо завершить свою работу и выполнить return. Рассмотрим пример:

функция main:

Создает контекст с функцией отмены

Вызывает функцию отмены по истечении произвольного таймаута

функция doWorkContext:

Создаёт производный контекст с тайм-аутом

Этот контекст отменяется, когда функция main вызывает cancelFunction, истекает таймаут, либо doWorkContext вызывет свою cancelFunction.

Запускает горутину для выполнения какой-нибудь медленной задачи, передавая полученный контекст

Ждет завершения горутины или отмены контекста от main, в зависимости от того, что произойдет первым

функция sleepRandomContext :

Запускает горутину для выполнения какой-нибудь медленной задачи

Ждет, пока завершится горутина, или

Ждет отмены контекста функцией main, таймаута или вызова своей собственной cancelFunction

функция sleepRandom:

Засыпает на рандомное время

В этом примере используется спящий режим для имитации случайного времени обработки, в реальности же вы можете использовать каналы, чтобы сигнализировать этой функции о начале очистки и ждать из канала подтверждения, что очистка завершена.

Песочница (Выглядит так, будто рандомное время, которое я использую в песочнице, практически не меняется. Попробуйте выполнить это на вашем локальном компьютере, чтобы увидеть случайность)

→ Github

package main

import (
"context"
"fmt"
"math/rand"
"Time"
)

// Медленная функция
func sleepRandom(fromFunction string, ch chan int) {
// Отложенная функция очистки
defer func() { fmt.Println(fromFunction, "sleepRandom complete") }()

// Выполним медленную задачу
// В качестве примера,
// «заснем» на рандомное время в мс
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
randomNumber := r.Intn(100)
sleeptime := randomNumber + 100

fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms")
time.Sleep(time.Duration(sleeptime) * time.Millisecond)
fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms")

// Напишем в канал, если он был передан
if ch != nil {
ch <- sleeptime
}
}

// Функция, выполняющая медленную работу с использованием контекста
// Заметьте, что контекст - это первый аргумент
func sleepRandomContext(ctx context.Context, ch chan bool) {

// Выполнение (прим. пер.: отложенное выполнение) действий по очистке
// Созданных контекстов больше нет
// Следовательно, отмена не требуется
defer func() {
fmt.Println("sleepRandomContext complete")
ch <- true
}()

// Создаем канал
sleeptimeChan := make(chan int)

// Запускаем выполнение медленной задачи в горутине
// Передаем канал для коммуникаций
go sleepRandom("sleepRandomContext", sleeptimeChan)

// Используем select для выхода по истечении времени жизни контекста
select {
case <-ctx.Done():
// Если контекст отменен, выбирается этот случай
// Это случается, если заканчивается таймаут doWorkContext или
// doWorkContext или main вызывает cancelFunction
// Высвобождаем ресурсы, которые больше не нужны из-за прерывания работы
// Посылаем сигнал всем горутинам, которые должны завершиться (используя каналы)
// Обычно вы посылаете что-нибудь в канал,
// ждете выхода из горутины, затем возвращаетесь
// Или используете группы ожидания вместо каналов для синхронизации
fmt.Println("sleepRandomContext: Time to return")

case sleeptime := <-sleeptimeChan:
// Этот вариант выбирается, когда работа завершается до отмены контекста
fmt.Println("Slept for ", sleeptime, "ms")
}
}

// Вспомогательная функция, которая в реальности может использоваться для разных целей
// Здесь она просто вызывает одну функцию
// В данном случае, она могла бы быть в main
func doWorkContext(ctx context.Context) {

// От контекста с функцией отмены создаём производный контекст с тайм-аутом
// Таймаут 150 мс
// Все контексты, производные от этого, завершатся через 150 мс
ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond)

// Функция отмены для освобождения ресурсов после завершения функции
defer func() {
fmt.Println("doWorkContext complete")
cancelFunction()
}()

// Создаем канал и вызываем функцию контекста
// Можно также использовать группы ожидания для этого конкретного случая,
// поскольку мы не используем возвращаемое значение, отправленное в канал
ch := make(chan bool)
go sleepRandomContext(ctxWithTimeout, ch)

// Используем select для выхода при истечении контекста
select {
case <-ctx.Done():
// Этот случай выбирается, когда переданный в качестве аргумента контекст уведомляет о завершении работы
// В данном примере это произойдёт, когда в main будет вызвана cancelFunction
fmt.Println("doWorkContext: Time to return")

case <-ch:
// Этот вариант выбирается, когда работа завершается до отмены контекста
fmt.Println("sleepRandomContext returned")
}
}

func main() {
// Создаем контекст background
ctx := context.Background()
// Производим контекст с отменой
ctxWithCancel, cancelFunction := context.WithCancel(ctx)

// Отложенная отмена высвобождает все ресурсы
// для этого и производных от него контекстов
defer func() {
fmt.Println("Main Defer: canceling context")
cancelFunction()
}()

// Отмена контекста после случайного тайм-аута
// Если это происходит, все производные от него контексты должны завершиться
go func() {
sleepRandom("Main", nil)
cancelFunction()
fmt.Println("Main Sleep complete. canceling context")
}()

// Выполнение работы
doWorkContext(ctxWithCancel)
}

Подводные камни

Если функция использует контекст, убедитесь, что уведомления об отмене обрабатываются должным образом. Например, что exec.CommandContext не закрывает канал чтения, пока команда не выполнит все форки, созданные процессом (Github), т.е., что отмена контекста не приведет к немедленному возврату из функции, если вы ждете с cmd.Wait(), пока все форки внешней команды не завершат обработку.

Если вы используете таймаут или дедлайн с максимальным временем выполнения, он может работать не так, как ожидается. В таких случаях, лучше реализовать таймауты с помощью time.After.

Лучшие практики

context.Background следует использовать только на самом высоком уровне, как корень всех производных контекстов.

context.TODO должен использоваться, когда вы не уверены, что использовать, или если текущая функция будет использовать контекст в будущем.

Отмены контекста рекомендуются, но эти функции могут занимать время, чтобы выполнить очистку и выход.

context.Value следует использовать как можно реже, и его нельзя применять для передачи необязательных параметров. Это делает API непонятным и может привести к ошибкам. Такие значения должны передаваться как аргументы.

Не храните контексты в структуре, передавайте их явно в функциях, предпочтительно в качестве первого аргумента.

Никогда не передавайте nil-контекст в качестве аргумента. Если сомневаетесь, используйте TODO.

Структура Context не имеет метода cancel, потому что только функция, которая порождает контекст, должна его отменять.

От переводчика

В нашей компании мы активно используем пакет Context при разработке серверных приложений для внутреннего использования. Но такие приложения для нормального функционирования, помимо Контекста, требуют дополнительных элементов, таких как:

Логирование

Обработка сигналов на завершение приложений, reload и logrotate

Работа с pid-файлами

Работа с конфигурационными файлами

И другое

Поэтому в какой-то момент мы решили обобщить весь накопленный нами опыт и создали вспомогательные пакеты, значительно упрощающие написание приложений (особенно приложений, имеющих API). Свои разработки мы выложили в открытый доступ и любой желающий может ими воспользоваться. Ниже приводятся некоторые ссылки на пакеты, полезные для решения таких задач:

nxs-go-appctx

nxs-go-conf

Также читайте другие статьи в нашем блоге:

Три простых приема для уменьшения Docker-образов

Бэкапы Stateful в Kubernetes

Резервное копирование большого количества разнородных web-проектов

Telegram-бот для Redmine. Как упростить жизнь себе и людям

Теги:

Golang

Go

Программирование

Хабы:

Блог компании Nixys

Go

Программирование

+11

202

5
+5

Nixys
DevOps, DevSecOps, MLOps — системный IT-интегратор

Подписаться

СайтВКонтактеTelegram

18

Карма

0

Рейтинг

Борис Ершов @borisershov

Технический директор

Подписаться

Комментарии 5
+5

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.2K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.7K

Кейс

Перевод

+27

21

2
+2

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.5K

Мнение

+22

17

63
+63

lokosst 13 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

25

0

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.2K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.7K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.6K

+16

5

6
+6

psygalaxy 14 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

30

9
+9

kubelet 17 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

Исследование: как и где ML-специалисты используют ИИ. Расскажите о своем опыте
Опрос

Показать еще

Минуточку внимания

Событие

Хабр приглашает маркетологов и пиарщиков на тест-драйв

Событие

Что ни день – то ивент, если следить за Календарём

Промо

Неважно, что ты выберешь — с Промокодусом будет дешевле

Информация

Сайт

nixys.ru

Дата регистрации

23 января 2018

Дата основания

3 февраля 2011

Численность

51–100 человек

Местоположение

Россия

Представитель

Vlada Grishkina-Makareva

Блог на Хабре

10 янв в 14:08

Из 2024 в 2025: вспоминаем лучшие практики CI/CD

18K

10
+10

11 окт 2024 в 09:40

Почтовый сервер с нуля. Часть третья

19K

20
+20

27 сен 2024 в 13:07

Спускаясь с облаков в ад: развёртывание Kubernetes на Astra Linux. Часть 1

18K

16
+16

30 авг 2024 в 14:32

Неудачные архитектурные решения при миграции в облако

3.5K

2
+2

24 авг 2024 в 14:12

Интеграция защищённого контура в Yandex Cloud: делимся опытом

3.8K

4
+4

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Реклама

ООО ЮФО ХОСТИНГ

ИНН 5043089443

erid: 2VSb5xF8vhf

VPS/VDS с 10 Gbps

Переходите на UFO.Hosting

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr